package com.xuecheng.media.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.xuecheng.base.exception.XuechengPlusException;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.base.model.RestResponse;
import com.xuecheng.media.mapper.MediaFilesMapper;
import com.xuecheng.media.mapper.MediaProcessMapper;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.model.po.MediaProcess;
import com.xuecheng.media.service.MediaFileService;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.*;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author Mr.M
 * @version 1.0
 * @description TODO
 * @date 2022/9/10 8:58
 */
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {

    @Autowired
    MediaFilesMapper mediaFilesMapper;
    @Autowired
    MinioClient minioClient;
    @Autowired
    MediaProcessMapper mediaProcessMapper;

    //获取普通文件的桶的名字
    @Value("${minio.bucket.files}")
    private String bucket_files;

    //获取视频文件桶的名字
    @Value("${minio.bucket.videofiles}")
    private String bucket_videoFiles;

    @Override
    public PageResult<MediaFiles> queryMediaFiels(Long companyId, PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {

        //构建查询条件对象
        LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(queryMediaParamsDto.getFilename() != null, MediaFiles::getFilename, queryMediaParamsDto.getFilename())
                .eq(queryMediaParamsDto.getAuditStatus() != null, MediaFiles::getAuditStatus, queryMediaParamsDto.getAuditStatus())
                .eq(queryMediaParamsDto.getType()!=null, MediaFiles::getFileType, queryMediaParamsDto.getType());

        //分页对象
        Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
        // 查询数据内容获得结果
        Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
        // 获取数据列表
        List<MediaFiles> list = pageResult.getRecords();
        // 获取数据总数
        long total = pageResult.getTotal();
        // 构建结果集
        PageResult<MediaFiles> mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
        return mediaListResult;

    }


    /**
     * @param companyId           机构id
     * @param uploadFileParamsDto 上传文件信息
     * @param localFilePath       文件磁盘路径
     * @return 文件信息
     * @description 上传文件
     */
    @Override
    @Transactional //涉及到两个库的操作，将文件上传到minio和将文件信息存储到数据库，要加事务
    public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath,String objectName) {
        //我们先要把本地文件上传到minio中，然后再将该文件信息存储到数据库中
        //先获取到本地路径的文件
        File file = new File(localFilePath);
        if (!file.exists()) {
            XuechengPlusException.cast("文件不存在");
        }
        //获取上传文件的文件名
        String filename = uploadFileParamsDto.getFilename();
        //获取文件的扩展名 来判断是普通文件还是视频文件
        //filename.substring(filename.lastIndexOf(".")) 从文件名中最后一个 "." 的位置开始截取，得到包含 "." 及其后面的部分，
        // 通常是文件的扩展名。
        String extension = filename.substring(filename.lastIndexOf("."));
        // 获取文件的mimeType
        String mimeType = getMimeType(extension);
        // 生成文件的md5值 统一格式为：年/月/日/
        String fileMd5 = getFileMd5(file);
        //文件的默认目录
        String defaultFolderPath = getDefaultFolderPath(fileMd5);
        // 存储到minio中的对象名（带目录）
        if(objectName==null){
            objectName = defaultFolderPath + fileMd5 + extension;
        }
        // 将文件上传到minio
        boolean b = addMediaFilesToMinIO(localFilePath, mimeType, bucket_files, objectName);

        //文件大小 按照文件字节的长度
        uploadFileParamsDto.setFileSize(file.length());
        //将文件信息存储到数据库中
        MediaFiles mediaFiles = addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_files, objectName);

        //准备返回数据
        UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
        BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
        return uploadFileResultDto;
    }

    /**
     * @param fileMd5 文件的md5
     * @return com.xuecheng.base.model.RestResponse<java.lang.Boolean> false不存在，true存在
     * @description 检查文件是否存在
     * @author Mr.M
     * @date 2022/9/13 15:38
     */
    @Override
    public RestResponse<Boolean> checkFile(String fileMd5) {
        //查询文件信息
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        if (mediaFiles != null) {
            //桶
            String bucket = mediaFiles.getBucket();
            //存储目录
            String filePath = mediaFiles.getFilePath();
            //文件流
            InputStream stream = null;
            try {
                //获取文件流
                stream = minioClient.getObject(
                        GetObjectArgs.builder()
                                .bucket(bucket)
                                .object(filePath)
                                .build());

                if (stream != null) {
                    //文件已存在
                    return RestResponse.success(true);
                }
            } catch (Exception e) {

            }
        }

        //文件不存在
        return RestResponse.success(false);
    }

    /**
     * @param fileMd5    文件的md5
     * @param chunkIndex 分块序号
     * @return com.xuecheng.base.model.RestResponse<java.lang.Boolean> false不存在，true存在
     * @description 检查分块是否存在
     * @author Mr.M
     * @date 2022/9/13 15:39
     */
    @Override
    public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
        //得到分块文件目录
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        //得到分块文件的路径
        String chunkFilePath = chunkFileFolderPath + chunkIndex;

        //文件流
        InputStream fileInputStream = null;
        try {
            fileInputStream = minioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(bucket_videoFiles)
                            .object(chunkFilePath)
                            .build());

            if (fileInputStream != null) {
                //分块已存在
                return RestResponse.success(true);
            }
        } catch (Exception e) {

        }
        //关闭流
        IOUtils.closeQuietly(fileInputStream);
        //分块未存在
        return RestResponse.success(false);
    }

    /**
     * @param fileMd5            文件md5
     * @param chunk              分块序号
     * @param localChunkFilePath 分块文件本地路径
     * @return com.xuecheng.base.model.RestResponse
     * @description 上传分块
     * @author Mr.M
     * @date 2022/9/13 15:50
     */
    @Override
    public RestResponse uploadChunk(String fileMd5, int chunk, String localChunkFilePath) {
        //得到分块文件的目录路径
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        //加上第几块构成完整路径
        String chunkFilePath = chunkFileFolderPath + chunk;
        //获取分块文件的mimeType
        String mimeType = getMimeType(null);
        //将文件上传到minio中
        boolean b = addMediaFilesToMinIO(localChunkFilePath, mimeType, bucket_videoFiles, chunkFilePath);
        if (b) {
            //文件上传成功
            log.debug("上传分块文件成功,{},{}", chunkFilePath, b);
            return RestResponse.success(true);
        } else {
            log.debug("上传分块文件失败,{},{}", chunkFilePath, b);
            return RestResponse.validfail(false, "上传分块文件失败");
        }
    }

    /**
     * @param companyId           机构id
     * @param fileMd5             文件md5
     * @param chunkTotal          分块总和
     * @param uploadFileParamsDto 文件信息
     * @return com.xuecheng.base.model.RestResponse
     * @description 合并分块
     * @author Mr.M
     * @date 2022/9/13 15:56
     */
    @Override
    public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {

        //如果文件存在则直接返回
        RestResponse<Boolean> response = checkFile(fileMd5);
        if(response.getResult()){
            return RestResponse.success(true);
        }
        //获取分块文件路径
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        //组成将文件文件路径组成List<ComposeSource>
        // 创建一个ComposeSource对象列表，用于定义组成源的集合
        //使用Stream.iterate生成从0到chunkTotal-1的整数流。
        //对每个整数i，构建一个ComposeSource对象，指定桶名为bucket_videoFiles，对象路径为chunkFileFolderPath + i。
        //将所有生成的ComposeSource对象收集到列表中。
        List<ComposeSource> sourceList = Stream.iterate(0, i -> ++i).limit(chunkTotal)
                .map(i -> ComposeSource.builder()
                        //桶
                        .bucket(bucket_videoFiles)
                        //分块文件路径
                        .object(chunkFileFolderPath.concat(Integer.toString(i)))
                        .build())
                .collect(Collectors.toList());

        //合并
        //文件名称
        String fileName = uploadFileParamsDto.getFilename();
        //扩展名
        String extName = fileName.substring(fileName.lastIndexOf("."));
        //合并文件路径
        String mergeFilePath = getFilePathByMd5(fileMd5, extName);
        try {
            //合并文件
            // 组合一个对象到另一个对象中
            minioClient.composeObject(
                    ComposeObjectArgs.builder()
                            .bucket(bucket_videoFiles) // 指定桶名称
                            .object(mergeFilePath) // 指定组合后的对象路径
                            .sources(sourceList) // 指定源对象列表
                            .build());
            log.info("合并文件成功:{}", mergeFilePath);
        } catch (Exception e) {
            log.debug("合并文件失败,fileMd5:{},异常:{}", fileMd5, e.getMessage(), e);
            return RestResponse.validfail(false, "合并文件失败。");
        }

        //验证两个MD5值是否相同
        //下载合并后的文件
        File minioFile = downloadFileFromMinIO(bucket_videoFiles, mergeFilePath);
        if (minioFile == null) {
            log.debug("下载合并文件失败,mergeFilePath:{}", mergeFilePath);
            return RestResponse.validfail(false, "下载合并文件失败。");
        }

        try (InputStream newFileInputStream = new FileInputStream(minioFile)) {
            //计算合并后文件的md5
            String mergeFileMd5 = DigestUtils.md5Hex(newFileInputStream);
            //比较md5值，不一致则说明文件不完整
            if (!fileMd5.equals(mergeFileMd5)) {
                return RestResponse.validfail(false, "文件合并校验失败，最终上传失败。");

            }
            //文件大小
            uploadFileParamsDto.setFileSize(minioFile.length());
        } catch (Exception e) {
            log.debug("校验文件失败,fileMd5:{},异常:{}",fileMd5,e.getMessage(),e);
            return RestResponse.validfail(false, "文件合并校验失败，最终上传失败。");
        } finally {
            if (minioFile != null) {
                minioFile.delete();
            }
        }

        //文件入库 等到分块合并之后再上传到数据库
        this.addMediaFilesToDb(companyId,fileMd5,uploadFileParamsDto,bucket_videoFiles,mergeFilePath);
        //=====清除分块文件=====
        clearChunkFiles(chunkFileFolderPath,chunkTotal);
        return RestResponse.success(true);
    }

    /***
     * 清除分块文件
     * @param chunkFileFolderPath
     * @param chunkTotal
     */
    private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal) {

        try {
            List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i)
                    .limit(chunkTotal)
                    .map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
                    .collect(Collectors.toList());

            RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket("video").objects(deleteObjects).build();
            Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
            results.forEach(r->{
                DeleteError deleteError = null;
                try {
                    deleteError = r.get();
                } catch (Exception e) {
                    e.printStackTrace();
                    log.error("清楚分块文件失败,objectname:{}",deleteError.objectName(),e);
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
            log.error("清楚分块文件失败,chunkFileFolderPath:{}",chunkFileFolderPath,e);
        }
    }

    /***
     * 从minio中下载指定路径的文件
     * @param bucket_videoFiles 桶
     * @param mergeFilePath 文件路径
     * @return
     */
    public File downloadFileFromMinIO(String bucket_videoFiles, String mergeFilePath) {
        // 临时文件
        File minioFile = null;
        // 创建临时文件
        FileOutputStream outputStream = null;
        try {
            // 从MinIO服务器获取对象（视频文件）的输入流
            // 这里的MinIO客户端方法用于下载存储在指定桶中的对象
            // 参数 bucket_videoFiles 指定了存放视频文件的桶名称
            // 参数 mergeFilePath 指定了要获取的对象（文件）的路径和名称
            InputStream stream = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucket_videoFiles)
                    .object(mergeFilePath)
                    .build());

            //创建临时文件
            // 创建一个名为"minio"、后缀为".merge"的临时文件，用于存储合并后的数据
            minioFile = File.createTempFile("minio", ".merge");
            // 创建一个文件输出流，用于将数据写入到临时文件中
            outputStream = new FileOutputStream(minioFile);
            // 使用IO工具类，将输入流中的数据复制到文件输出流中，实现数据的合并与存储
            IOUtils.copy(stream, outputStream);
            // 返回存储了合并数据的临时文件对象
            return minioFile;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    /***
     * 根据文件md5得到文件路径
     * @param fileMd5
     * @param extName
     * @return
     */
    private String getFilePathByMd5(String fileMd5, String extName) {
        return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5 + extName;
    }

    // 得到分块文件的目录
    //substring(0, 1) 表示从索引 0 开始，到索引 1 结束（不包括索引 1 的字符）。
    //：如果 fileMd5 = "abcdef1234567890"，最终结果为：a/b/abcdef1234567890/chunk/
    private String getChunkFileFolderPath(String fileMd5) {
        return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
    }

    /**
     * 将文件信息存储到数据库中
     *
     * @param companyId           机构id
     * @param fileMd5             文件的md5值
     * @param uploadFileParamsDto 文件信息
     * @param bucket_files        桶
     * @param objectName          对象名（也就是路径）
     * @return 存储到数据库中的文件信息
     */
    @Transactional
    public MediaFiles addMediaFilesToDb(Long companyId, String fileMd5, UploadFileParamsDto uploadFileParamsDto, String bucket_files, String objectName) {
        //从数据库查询文件
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        if (mediaFiles == null) {
            mediaFiles = new MediaFiles();
            //拷贝基本信息
            BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);
            mediaFiles.setId(fileMd5);
            mediaFiles.setFileId(fileMd5);
            mediaFiles.setCompanyId(companyId);
            mediaFiles.setUrl("/" + bucket_files + "/" + objectName);
            mediaFiles.setBucket(bucket_files);
            mediaFiles.setFilePath(objectName);
            mediaFiles.setCreateDate(LocalDateTime.now());
            mediaFiles.setAuditStatus("002003");
            mediaFiles.setStatus("1");
            //保存文件信息到文件表
            int insert = mediaFilesMapper.insert(mediaFiles);
            if (insert < 0) {
                log.error("保存文件信息到数据库失败,{}", mediaFiles.toString());
                XuechengPlusException.cast("保存文件信息失败");
            }
            log.debug("保存文件信息到数据库成功,{}", mediaFiles.toString());

            //将文件信息保存到待处理任务表
            addWaitingTask(mediaFiles);
            log.debug("保存文件信息到数据库成功,{}", mediaFiles.toString());
        }
        return mediaFiles;
    }

    /**
     * 将文件信息保存到待处理任务表
     * @param mediaFiles
     */
    private void addWaitingTask(MediaFiles mediaFiles) {

        //获取文件名
        String filename = mediaFiles.getFilename();
        //获取后缀名
        String extension = filename.substring(filename.lastIndexOf("."));

        //根据后缀名得到他的mimetype
        String mimeType = getMimeType(extension);

        //如果是avi视频的话添加到待处理avi任务表 我们的视频时wmv类型
        if ("video/x-msvideo".equals(mimeType) || "video/x-ms-wmv".equals(mimeType)) {
            MediaProcess mediaProcess = new MediaProcess();
            BeanUtils.copyProperties(mediaFiles, mediaProcess);
            mediaProcess.setStatus("1");
            mediaProcess.setFailCount(0);
            mediaProcess.setCreateDate(LocalDateTime.now());
            mediaProcessMapper.insert(mediaProcess);
        }
    }

    /**
     * 将文件上传到minio
     *
     * @param localFilePath 本地文件路径
     * @param mimeType      文件类型
     * @param bucket_files  桶
     * @param objectName    对象名 也就是存储的路径
     * @return 是否成功
     */
    public boolean addMediaFilesToMinIO(String localFilePath, String mimeType, String bucket_files, String objectName) {

        try {
            //获取minio客户端
            UploadObjectArgs bucket = UploadObjectArgs.builder()
                    .bucket(bucket_files)
                    .object(objectName)
                    .filename(localFilePath)
                    .contentType(mimeType)
                    .build();
            minioClient.uploadObject(bucket);
            log.debug("上传文件到minio成功,bucket:{},objectNmae:{}", bucket_files, objectName);
            System.out.println("上传成功");
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("上传文件到minio出错,bucket:{},objectName:{},错误原因:{}", bucket_files, objectName, e.getMessage(), e);
            XuechengPlusException.cast("上传文件到文件系统失败");
        }
        return false;
    }

    /**
     * 获取文件默认存储目录路径 年/月/日
     *
     * @param fileMd5
     * @return
     */
    private String getDefaultFolderPath(String fileMd5) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String folder = sdf.format(new Date()).replace("-", "/") + "/";
        return folder;
    }

    /**
     * 获取文件的md5
     *
     * @param file
     * @return
     */
    private String getFileMd5(File file) {
        try (FileInputStream fileInputStream = new FileInputStream(file)) {
            String fileMd5 = DigestUtils.md5Hex(fileInputStream);
            return fileMd5;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 根据扩展名获取mimeType
     *
     * @param extension
     * @return
     */
    private String getMimeType(String extension) {
        //如果为空null 那么就给个默认
        if (extension == null) {
            extension = "";
        }

        //根据扩展名取出mimeType
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);

        //如果为空，就设置为字节流 默认的
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        //获取对应的mimeType
        if (extensionMatch != null) {
            mimeType = extensionMatch.getMimeType();
        }
        return mimeType;
    }
}
